package space.edge.d.demo.qrcode.privatecode.detector;

import android.util.Log;

import space.edge.d.demo.qrcode.common.BitMatrix;
import space.edge.d.demo.qrcode.common.MathUtils;
import space.edge.d.demo.qrcode.common.ResultPoint;
import space.edge.d.demo.qrcode.common.ResultPointCallback;
import space.edge.d.demo.qrcode.common.DecodeHintType;
import space.edge.d.demo.qrcode.exception.FormatException;
import space.edge.d.demo.qrcode.exception.NotFoundException;
import space.edge.d.demo.qrcode.imageproc.GridSampler;
import space.edge.d.demo.qrcode.imageproc.PerspectiveTransform;
import space.edge.d.demo.qrcode.privatecode.ui.CodeRenderer;

import java.util.Map;

/**
 * Created by gsl on 2017/9/14.
 * 用于检测四个定位符
 */

public class Detector {

    private final String TAG = Detector.class.getSimpleName();

    private BitMatrix image;
    private ResultPointCallback mCallback;

    public Detector(BitMatrix img) {
        image = img;
    }

    public DetectorResult detect(Map<DecodeHintType, ?> hints) throws NotFoundException, FormatException {
        mCallback = hints == null ? null : (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK);

        FinderPatternFinder finderPatternFinder = new FinderPatternFinder(image, mCallback);
        FinderPatternInfo info = finderPatternFinder.find();
        return processFinderPatternInfo(info);
    }

    private DetectorResult processFinderPatternInfo(FinderPatternInfo info)
            throws NotFoundException, FormatException {

        FinderPattern topLeft = info.getTopLeft();
        FinderPattern topRight = info.getTopRight();
        FinderPattern bottomLeft = info.getBottomLeft();

        //这里根据定位符大小计算出了二维码图像中一行或一列所占像素数量
        float moduleSize = calculateModuleSize(topLeft, topRight, bottomLeft);
        if (moduleSize < 1.0f) {
            throw NotFoundException.getNotFoundInstance();
        }

        // Guess where a "bottom right" finder pattern would have been
        // 根据已知的三个定位符的位置，估算出右下角定位符的位置，进而方便计算出辅助定位符的位置
        int bottomRightX = (int) (topRight.getX() - topLeft.getX() + bottomLeft.getX()) + 5;
        int bottomRightY = (int) (topRight.getY() - topLeft.getY() + bottomLeft.getY()) + 5;

        // Estimate that alignment pattern is closer by 3 modules
        // from "bottom right" to known top left location
        // 因为辅助定位符真正的中心点是要比定位符中心点向QRCode中心偏移3的，所以这里的计算是为了较正偏移
        // 直接去搜索辅助定位符，根据定位符算出来辅助定位符的大致大小，然后确定搜索半径，辅助定位符是普通定位符的两倍
        // 搜索半径确定为辅助定位符的一个直径，然后是1.5倍直径
        // 我们的辅助定位符大小为辅助定位符的两倍，所以估算出来的右下角的点很大概率落在定位符内部，进而可以找到真正的中心点
        AlignmentPattern pattern;
        try {
            pattern = findAlignmentInRegion(moduleSize, bottomRightX, bottomRightY, 2);
        } catch (NotFoundException ignored) {
            pattern = findAlignmentInRegion(moduleSize, bottomRightX, bottomRightY, 3);
            Log.e(TAG, "alignment not found , expand search radius and retry");
        }
        PerspectiveTransform transform =
                createTransform(topLeft, topRight, bottomLeft, pattern, CodeRenderer.CODE_SIZE);
        BitMatrix bits = sampleGrid(image, transform, CodeRenderer.CODE_SIZE);

        // 后续步骤
        // TODO: 2017/10/16 1.要进行滤波，但是可能产生噪声的类型的该怎么处理，这里只实现了简单的8邻域滤波
        // TODO: 2017/10/16 2.去除掉定位符以及定位符空白区域的多余像素，可以与模板进行与运算，达到去除掺杂像素的目的
        ResultPoint[] points;
        if (pattern == null) {
            points = new ResultPoint[]{topLeft, topRight, bottomLeft};
        } else {
            points = new ResultPoint[]{topLeft, topRight, bottomLeft, pattern};
        }
        return new DetectorResult(bits, points);
    }

    private static BitMatrix sampleGrid(BitMatrix image,
                                        PerspectiveTransform transform,
                                        int dimension) throws NotFoundException {

        GridSampler sampler = GridSampler.getInstance();
        return sampler.sampleGrid(image, dimension, dimension, transform);
    }

    private static PerspectiveTransform createTransform(ResultPoint topLeft,
                                                        ResultPoint topRight,
                                                        ResultPoint bottomLeft,
                                                        ResultPoint alignmentPattern,
                                                        int dimension) {
        float dimMinusThree = dimension - 105;
        float bottomRightX;
        float bottomRightY;
        float sourceBottomRightX;
        float sourceBottomRightY;
        if (alignmentPattern != null) {
            bottomRightX = alignmentPattern.getX();
            bottomRightY = alignmentPattern.getY();
            sourceBottomRightX = dimMinusThree + 5;
            sourceBottomRightY = sourceBottomRightX;
        } else {
            // Don't have an alignment pattern, just make up the bottom-right point
            bottomRightX = (topRight.getX() - topLeft.getX()) + bottomLeft.getX();
            bottomRightY = (topRight.getY() - topLeft.getY()) + bottomLeft.getY();
            sourceBottomRightX = dimMinusThree;
            sourceBottomRightY = dimMinusThree;
        }

        return PerspectiveTransform.quadrilateralToQuadrilateral(
                105,
                105,
                dimMinusThree,
                105,
                sourceBottomRightX,
                sourceBottomRightY,
                105,
                dimMinusThree,
                topLeft.getX(),
                topLeft.getY(),
                topRight.getX(),
                topRight.getY(),
                bottomRightX,
                bottomRightY,
                bottomLeft.getX(),
                bottomLeft.getY());
    }

    /**
     * <p>Computes an average estimated module size based on estimated derived from the positions
     * of the three finder patterns.</p>
     * 计算单边宽度，分别计算竖直&水平方向然后取平均值
     *
     * @param topLeft    detected top-left finder pattern center
     * @param topRight   detected top-right finder pattern center
     * @param bottomLeft detected bottom-left finder pattern center
     * @return estimated module size
     */
    private float calculateModuleSize(ResultPoint topLeft,
                                      ResultPoint topRight,
                                      ResultPoint bottomLeft) {
        // Take the average
        return (calculateModuleSizeOneWay(topLeft, topRight) +
                calculateModuleSizeOneWay(topLeft, bottomLeft)) / 2.0f;
    }

    /**
     * <p>Estimates module size based on two finder patterns -- it uses
     * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the
     * width of each, measuring along the axis between their centers.</p>
     * 计算出一个圆形定位符的直径，由于是圆形定位符，所以在扫描的时候感觉
     */
    private float calculateModuleSizeOneWay(ResultPoint pattern, ResultPoint otherPattern) {
        float moduleSizeEst1 = sizeOfBlackWhiteBlackRunBothWays((int) pattern.getX(),
                (int) pattern.getY(),
                (int) otherPattern.getX(),
                (int) otherPattern.getY());
        float moduleSizeEst2 = sizeOfBlackWhiteBlackRunBothWays((int) otherPattern.getX(),
                (int) otherPattern.getY(),
                (int) pattern.getX(),
                (int) pattern.getY());
        if (Float.isNaN(moduleSizeEst1)) {
            return moduleSizeEst2;
        }
        if (Float.isNaN(moduleSizeEst2)) {
            return moduleSizeEst1;
        }
        // Average them, and divide by 7 since we've counted the width of 3 black modules,
        // and 1 white and 1 black module on either side. Ergo, divide sum by 14.
        return (moduleSizeEst1 + moduleSizeEst2) / 2.0f;
    }

    /**
     * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of
     * a finder pattern by looking for a black-white-black run from the center in the direction
     * of another point (another finder pattern center), and in the opposite direction too.
     */
    private float sizeOfBlackWhiteBlackRunBothWays(int fromX, int fromY, int toX, int toY) {

        float result = sizeOfBlackWhiteBlackRun(fromX, fromY, toX, toY);

        // Now count other way -- don't run off image though of course
        float scale = 1.0f;
        int otherToX = fromX - (toX - fromX);
        if (otherToX < 0) {
            scale = fromX / (float) (fromX - otherToX);
            otherToX = 0;
        } else if (otherToX >= image.getWidth()) {
            scale = (image.getWidth() - 1 - fromX) / (float) (otherToX - fromX);
            otherToX = image.getWidth() - 1;
        }
        int otherToY = (int) (fromY - (toY - fromY) * scale);

        scale = 1.0f;
        if (otherToY < 0) {
            scale = fromY / (float) (fromY - otherToY);
            otherToY = 0;
        } else if (otherToY >= image.getHeight()) {
            scale = (image.getHeight() - 1 - fromY) / (float) (otherToY - fromY);
            otherToY = image.getHeight() - 1;
        }
        otherToX = (int) (fromX + (otherToX - fromX) * scale);

        result += sizeOfBlackWhiteBlackRun(fromX, fromY, otherToX, otherToY);

        // Middle pixel is double-counted this way; subtract 1
        return result - 1.0f;
    }

    /**
     * <p>This method traces a line from a point in the image, in the direction towards another point.
     * It begins in a black region, and keeps going until it finds white, then black, then white again.
     * It reports the distance from the start to this point.</p>
     * <p>
     * <p>This is used when figuring out how wide a finder pattern is, when the finder pattern
     * may be skewed or rotated.</p>
     * 该方法用于计算定位符的实际大小，即使图像发生了歪斜或者旋转
     */
    private float sizeOfBlackWhiteBlackRun(int fromX, int fromY, int toX, int toY) {
        // Mild variant of Bresenham's algorithm;
        // see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
        // Bresenham直线算法，如果斜率>1，则置换x，y轴坐标，也就是说这种算法只计算斜率在1之内的直线
        boolean steep = Math.abs(toY - fromY) > Math.abs(toX - fromX);
        if (steep) {
            int temp = fromX;
            fromX = fromY;
            fromY = temp;
            temp = toX;
            toX = toY;
            toY = temp;
        }

        int dx = Math.abs(toX - fromX);
        int dy = Math.abs(toY - fromY);
        int error = -dx / 2;
        int xstep = fromX < toX ? 1 : -1;
        int ystep = fromY < toY ? 1 : -1;

        // In black pixels, looking for white, first or second time.
        int state = 0;
        // Loop up until x == toX, but not beyond
        int xLimit = toX + xstep;
        for (int x = fromX, y = fromY; x != xLimit; x += xstep) {
            int realX = steep ? y : x;
            int realY = steep ? x : y;

            // Does current pixel mean we have moved white to black or vice versa?
            // Scanning black in state 0,2 and white in state 1, so if we find the wrong
            // color, advance to next state or end if we are in state 2 already
            if ((state == 1) == image.get(realX, realY)) {
                if (state == 2) {
                    return MathUtils.distance(x, y, fromX, fromY);
                }
                state++;
            }

            error += dy;
            if (error > 0) {
                if (y == toY) {
                    break;
                }
                y += ystep;
                error -= dx;
            }
        }
        // Found black-white-black; give the benefit of the doubt that the next pixel outside the image
        // is "white" so this last point at (toX+xStep,toY) is the right ending. This is really a
        // small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this.
        if (state == 2) {
            return MathUtils.distance(toX + xstep, toY, fromX, fromY);
        }
        // else we didn't find even black-white-black; no estimate is really possible
        return Float.NaN;
    }

    private AlignmentPattern findAlignmentInRegion(float overallEstModuleSize,
                                                   int estAlignmentX,
                                                   int estAlignmentY,
                                                   float allowanceFactor)
            throws NotFoundException {
        // Look for an alignment pattern (3 modules in size) around where it
        // should be
        // 这里计算出搜索区域的上下左右边界
        int allowance = (int) (allowanceFactor * overallEstModuleSize);
        int alignmentAreaLeftX = Math.max(0, estAlignmentX - allowance);
        int alignmentAreaRightX = Math.min(image.getWidth() - 1, estAlignmentX + allowance);
        if (alignmentAreaRightX - alignmentAreaLeftX < overallEstModuleSize) {
            throw NotFoundException.getNotFoundInstance();
        }

        int alignmentAreaTopY = Math.max(0, estAlignmentY - allowance);
        int alignmentAreaBottomY = Math.min(image.getHeight() - 1, estAlignmentY + allowance);
        if (alignmentAreaBottomY - alignmentAreaTopY < overallEstModuleSize) {
            throw NotFoundException.getNotFoundInstance();
        }

        AlignmentPatternFinder finder = new AlignmentPatternFinder(image, alignmentAreaLeftX, alignmentAreaTopY, alignmentAreaRightX - alignmentAreaLeftX,
                alignmentAreaBottomY - alignmentAreaTopY, estAlignmentX, estAlignmentY, mCallback);
        return finder.find();
    }
}
